package ch.utilities;

import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import ch.exception.BadHTTPRequestException;


/** Classe per il parsing di una richiesta HTTP.
*
* @author Neil Otupacca - May 28 2011
*
*/
public class HTTPRequestParser {

	private boolean flPostRequest;
	private String protocolName;
	private String hostName;
	private int port;
	private String resourcePath;
	private HashMap<String, String> requestHeaders;
	private HashMap<String, String> requestVariables;

	/** Il <i>constructor</i> esegue il parsing della request HTTP.
	 *
	 * @param httpRequest - stringa di HTTP request.
	 */
	public HTTPRequestParser(String httpRequest) {
		// group(1) = GET oppure POST (HTTP method)
		// group(2) = http oppure https (protocol)
		// group(3) = hostname
		// group(4) = port (opzionale)
		// group(5) = path + eventuale filename (opzionale)
		// group(6) = query string (opzionale)
		// group(7) = header + eventuale POST data
		String re = "^(GET|POST) (https?)://([^/:]+)(?::(\\d+))?/([^\\? ]+)?(?:\\?([^ ]+))? " +
					"HTTP/1\\.[01][\r\n]+(.+)";
		Pattern pat = Pattern.compile(re, Pattern.DOTALL);
		Matcher matcher = pat.matcher(httpRequest);

		// Errore di formato
		if (!matcher.find()) throw new BadHTTPRequestException("Format error");

		flPostRequest = (matcher.group(1).equals("POST")) ? true : false;
		protocolName = matcher.group(2);
		hostName = matcher.group(3);
		port = (matcher.group(4) != null) ? (new Integer(matcher.group(4)).intValue()) :
											((protocolName.equals("https")) ? 443 : 80);
		resourcePath = (matcher.group(5) == null) ? "" : matcher.group(5);

		// Se il filename non è specificato (solo il path), concatena
		// il nome di default "index.html"
		if (resourcePath.isEmpty() ||
			resourcePath.substring(resourcePath.length() - 1, resourcePath.length()).equals("/")) {
			resourcePath += "index.html";
		}

		String variables = (matcher.group(6) == null) ? "" : matcher.group(6);
		String headers = matcher.group(7);

		// Se la request è un POST, separa gli headers dal body di POST
		if (flPostRequest) {
			String reHeads = "^(.+?)(?:\r\n?+|\n\r?+){2}(.{3,})$";
			Pattern patHeads = Pattern.compile(reHeads, Pattern.DOTALL);
			Matcher matcherHeads = patHeads.matcher(matcher.group(7));

			// Errore di formato
			if (!matcherHeads.find()) throw new BadHTTPRequestException("No POST data");

			headers = matcherHeads.group(1);

			// Se il body POST non è empty, concatenalo alle variabili GET (query-string)
			if (matcherHeads.group(2).length() > 0) {
				if (variables.length() > 0) variables += "&";
				variables += matcherHeads.group(2);
			}
		}

		requestHeaders = parseHeaders(headers);
		requestVariables = parseGetPostVariables(variables);

		// Se il method HTTP è POST, accertarsi che "Content-Type:" non sia
		// diverso da "application/x-www-form-urlencoded" ("multipart/form-data"
		// non supportato)
		if (flPostRequest) {
			String contentType = getHeaderValue("Content-Type");

			if (!contentType.equals("application/x-www-form-urlencoded") &&
				contentType.length() > 0) throw new BadHTTPRequestException("Content-Type not supported");
		}
	}

	/** Esegue il parsing dell'header della richiesta HTTP e ritorna
	 * le coppie headerName=headerValue in una hashMap.
	 *
	 * @param headers - stringa degli headers.
	 * @return lista degli headers.
	 */
	protected static HashMap<String, String> parseHeaders(String headers) {
		HashMap<String, String> headersMap = new HashMap<String, String>();
		String re = "^([^:\r\n]+):[ \t]*(.*?)\\s*?(?=(?:[\n\r]+\\S|\\Z))";
		Pattern pat = Pattern.compile(re, Pattern.DOTALL | Pattern.MULTILINE);
		Matcher matcher = pat.matcher(headers);

		// Errore se non è stato trovato nessun header
		if (!matcher.find()) throw new BadHTTPRequestException("No headers found");

		headersMap.put(matcher.group(1), matcher.group(2).trim());

		// Itera gli headers seguenti
		while(matcher.find()) {
			headersMap.put(matcher.group(1), matcher.group(2).trim());
		}

		return(headersMap);
	}

	/** Esegue il parsing delle variabili della richiesta HTTP e ritorna
	 * le coppie varName=varValue in una hashMap.
	 *
	 * @param variables - stringa delle variabili.
	 * @return lista delle variabili.
	 */
	public static HashMap<String, String> parseGetPostVariables(String variables) {
		HashMap<String, String> varsMap = new HashMap<String, String>();
		String arrVars[] = variables.split("&");

		for ( int n = 0 ; n < arrVars.length ; n++ ) {
			int pos = arrVars[n].indexOf("=");
			if (pos < 1) continue;

			varsMap.put(arrVars[n].substring(0, pos), unescapeValues(arrVars[n].substring(pos + 1)));
		}

		return(varsMap);
	}

	/** Ritorna la copia di <b>escapedValue</b> con gli escapes %xx convertiti
	 * nel relativo carattere.
	 *
	 * @param escapedValue
	 * @return
	 */
	public static String unescapeValues(String escapedValue) {
		String re = "%([0-9a-f]{2})";
		Pattern pat = Pattern.compile(re, Pattern.CASE_INSENSITIVE);
		Matcher matcher = pat.matcher(escapedValue);

		String unescapedValue = escapedValue;
		int srcPos = 0;

		// Itera gli escapes nella stringa escapedValue
		while(matcher.find()) {
			unescapedValue = unescapedValue.substring(0, matcher.start() + srcPos) +
							 Character.toString((char) Integer.parseInt(matcher.group(1), 16)) +
							 escapedValue.substring(matcher.end());

			// I tre caratteri dell'escape vengono sostituiti con il
			// corrispondente carattere letterale, aggiorna l'offset per
			// compensare il gap tra la stringa escapedValue e unescapedValue
			srcPos -= 2;
		}

		return(unescapedValue);
	}

	/** Ritorna <b>true</b> se il method della richiesta HTTP è un POST.
	 *
	 * @return HTTP method.
	 */
	public boolean isPostRequest() {
		return(flPostRequest);
	}

	/** Ritorna il protocollo della richiesta HTTP.
	 *
	 * @return protocollo.
	 */
	public String getProtocol() {
		return(protocolName);
	}

	/** Ritorna l'hostname della richiesta HTTP.
	 *
	 * @return hostname.
	 */
	public String getHostname() {
		return(hostName);
	}

	/** Ritorna il <i>port</i> utilizzato dal protocollo.
	 *
	 * @return port.
	 */
	public int getPort() {
		return(port);
	}

	/** Ritorna il path della risorsa richiesta (incluso il filename).
	 *
	 * @return pathname.
	 */
	public String getResourcePath() {
		return(resourcePath);
	}

	/** Ritorna la lista di headers della richiesta HTTP.
	 *
	 * @return lista degli headers.
	 */
	@SuppressWarnings("unchecked")
	public HashMap<String, String> getHeadersList() {
		return((HashMap<String, String>) requestHeaders.clone());
	}

	/** Ritorna il valore dell'header corrispondente a <b>headerName</b>.
	 *
	 * @param headerName - nome dell'header.
	 * @return valore dell'header.
	 */
	public String getHeaderValue(String headerName) {
		return((requestHeaders.containsKey(headerName)) ? requestHeaders.get(headerName) : "");
	}

	/** Ritorna la lista delle variabili GET e POST della richiesta HTTP.
	 *
	 * @return lista delle variabili.
	 */
	@SuppressWarnings("unchecked")
	public HashMap<String, String> getVariablesList() {
		return((HashMap<String, String>) requestVariables.clone());
	}

	/** Ritorna il valore della variabile GET/POST corrispondente a <b>variableName</b>.
	 *
	 * @param variableName - nome della variabile.
	 * @return valore della variabile.
	 */
	public String getVariableValue(String variableName) {
		return((requestVariables.containsKey(variableName)) ? requestVariables.get(variableName) : "");
	}
}
